iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
1

這絕對不是向獵人致敬的標題哦XD
https://pic.pimg.tw/kiki6093331/1422842577-2377818836_m.jpg
是說是不是看過獵人又是一種年紀的象徵

咳咳..... 前兩天的文章中,
拿了 JSON 格式的資料來練習取資料跟應用的感覺,
可是你會說,
我總不能每次都傻傻的去用高級手工藝把資料貼到 Excel 再轉成 JSON 格式,
再把它貼到 JavaScript 宣告一個物件陣列才能用,
這樣不是很麻煩嗎orz 一點都沒有程式自動化的感覺.....

對,沒錯,
所以線上其實很多 API 服務,
當你去跟該 API 問資料,
它也會回你 JSON 或其他的格式資料,
你就可以很方便拿來應用啦:D
事不宜遲,讓我們趕緊動手吧!

正片開始

今日素材:口罩剩餘數量資料集

雖然現在實名制口罩大家應該都不搶了,
不過前一陣子很多工程師都發揮長才利用這個資料集在做口罩地圖,
所以今天就讓我們來試個吧!

然後現在能搜得到的都是已經用資料集所呈現的口罩地圖,
我之前找到現在還有在更新的資料集在這邊,
看起來好像是好心大大轉過一手放在 GitHub 的? → 口罩數量資料集
在這邊感恩提供資料的大大~~~

*重要*養成良好習慣:邏輯寫好才改成動態去問資料

如果程式已經寫的很熟練的大大當然不在此限,
只是之前小的在寫這種動態拿資料回來再做處理的,
很常是資料拿到後,後面處理資料的邏輯沒寫好,
但我前面一直存取,
然後就被視為攻擊了擋了XD
(因為 API 通常怕被攻擊都會有某段時間存取次數上限值之類的,
通常都要被擋個 N 分鐘後才能再次去問該 API 問資料回來orz)
為了避免這種事發生,
小的後來都習慣先把該 API 的 JSON 檔拿回來,
先存成物件陣列,
等到後面邏輯都寫好沒問題後,
前面才改成動態問資料。

先靜態取資料來玩吧!

所以先讓我們到 口罩數量資料集
看看資料長什麼樣子吧!
https://ithelp.ithome.com.tw/upload/images/20200923/20129873aa67R4WGZC.png

看起來有藥局的名字、電話、地址,還有最重要的
成人口罩剩餘數 → "mask_adult": 136,
兒童口罩剩餘數 → "mask_child": 433,

然後你也會觀察到每筆資料其實是包在 "features",
因此我們要靜態拿資料時,只要取 "features" 以下就好,最開頭的 "type" 不用取。
先取個 3 筆放到 JavaScript 並宣告:

let maskdata = [{
        "type": "Feature",
        "properties": {
            "id": "5945030094",
            "name": "德興藥局",
            "phone": "(03)8889408",
            "address": "花蓮縣玉里鎮國武里中山路2段58號",
            "mask_adult": 136,
            "mask_child": 433,
            "updated": "2020\/09\/23 20:41:48",
            "available": "星期一上午看診、星期二上午看診、星期三上午看診、星期四上午看診、星期五上午看診、星期六上午看診、星期日上午看診、星期一下午看診、星期二下午看診、星期三下午看診、星期四下午看診、星期五下午看診、星期六下午看診、星期日下午看診、星期一晚上看診、星期二晚上看診、星期三晚上看診、星期四晚上看診、星期五晚上看診、星期六晚上看診、星期日晚上看診",
            "note": "口罩販售,營業時間,成人口罩200份,兒童口罩20份,售完為止。",
            "custom_note": "",
            "website": "",
            "county": "花蓮縣",
            "town": "玉里鎮",
            "cunli": "國武里",
            "service_periods": "NNNNNNNNNNNNNNNNNNNNN"
        },
        "geometry": {
            "type": "Point",
            "coordinates": [
                121.315149,
                23.333096
            ]
        }
    },
// ... 中略 ...
];

然後也是 console.log 來看看:

console.log(`有 ${maskdata.length} 筆資料`);
console.log(`${maskdata[0].name} 還有 ${maskdata[0].mask_adult} 個成人口罩`);

https://ithelp.ithome.com.tw/upload/images/20200923/20129873eSMs298ACE.png
嗯?為什麼顯示 undefined?
再觀察一次資料結構,
哦~原來 name, mask_adult 等資料是被包在 properties 裡面呀!
來改一下:

console.log(`有 ${maskdata.length} 筆資料`);
console.log(`${maskdata[0].properties.name} 還有 ${maskdata[0].properties.mask_adult} 個成人口罩`);

https://ithelp.ithome.com.tw/upload/images/20200923/20129873KXVNbJBkNq.png
對了!之後如果還有類似的問題發生,
可以仔細觀察看看資料結構長怎樣哦!

拿資料運用總是要有個目的

那我們今日課題定為要撈出剩餘某數量以上的口罩有哪些好了!
例如這邊我先找出成人口罩 >100 的有哪幾家:

for ( let i=0; i<maskdata.length; i++ ){
    if ( maskdata[i].properties.mask_adult > 100 ){
        console.log(`${maskdata[i].properties.name} 還有 ${maskdata[i].properties.mask_adult} 個成人口罩`);
    }
}

https://ithelp.ithome.com.tw/upload/images/20200923/201298737NF1MVD9f5.png
初步判斷出來了,再來要把 100 改成可動態輸入。

本日課題:撈出剩餘指定數量(以上)的藥局

要先在 html 放入輸入數字的 input,以及送出的按鈕。
(PS. input 要給予 id,這樣我們才能透過 getElementById 取得元件再做進一步處理)

<input type="number" id="inputMaskNum">
<input type="button" value="送出" id="sendBtn">

再來複習一下過去兩天的內容 getElementById & addEventListener:

const inputNumElement = document.getElementById("inputMaskNum");
const sendBtnElement = document.getElementById("sendBtn");

sendBtnElement.addEventListener("click", getInputValue);

function getInputValue(){
    console.log("點到送出按鈕了");
}

https://ithelp.ithome.com.tw/upload/images/20200923/20129873Yt9pfXbWqY.png

再來我們要在點擊送出按鈕時,取得輸入框所輸入的數字:

function getInputValue(){
    console.log("點到送出按鈕了");
    console.log(`輸入的值為: ${inputNumElement.value}`);
}

https://ithelp.ithome.com.tw/upload/images/20200923/20129873qyXZdcnXHy.png
但還記得昨天有遇到一個情況,type 的問題,
所以順便檢查一下 input 取到值的 type 是什麼:

console.log(`輸入的值 type 為: ${typeof(inputNumElement.value)}`);

https://ithelp.ithome.com.tw/upload/images/20200923/20129873Ze8ArYtCH6.png
抓到!之後大家都要記得 input 相關的值初始都會是 string 型態,
所以要當數字處理的話記得要轉一手。

然後把前面寫過的邏輯也合在一起試試看:

let inputNum = parseInt(inputNumElement.value);
for ( let i=0; i<maskdata.length; i++ ){
    if ( maskdata[i].properties.mask_adult > inputNum ){
        console.log(`${maskdata[i].properties.name} 還有 ${maskdata[i].properties.mask_adult} 個成人口罩`);
    }
}

https://ithelp.ithome.com.tw/upload/images/20200923/20129873LMALG7nkY4.png
當我輸入 700,
真的撈出只口罩剩餘數量大於 700 的藥局,
本日要求大概 87% 達成了,
剩下的就是要將結果呈現在網頁上,
而不是 console.log 而已。

html 再宣告一個元素:

<ol id="filterResult"></ol>

再複習一下 innerHTML 的用法:

const filterResultElement = document.getElementById("filterResult");
let resultHTMLStr = "";
for ( let i=0; i<maskdata.length; i++ ){
    if ( maskdata[i].properties.mask_adult > inputNum ){
        resultHTMLStr += `<li>${maskdata[i].properties.name} 還有 ${maskdata[i].properties.mask_adult} 個成人口罩</li>`;
    }
}
filterResultElement.innerHTML = resultHTMLStr;

https://ithelp.ithome.com.tw/upload/images/20200923/201298737syHzSFHvo.png
好了,看起來很棒!
但不要忘記我們最初的目的,
就是要將最前面資料的地方改成動態問資料回來!

動態問資料回來

這邊的語法不用背,直接 copy 再改成今日要存取的 URL 即可:

// 拿即時更新的 JSON 檔
let requestURL = "https://raw.githubusercontent.com/kiang/pharmacies/master/json/points.json";
let xhr = new XMLHttpRequest();
xhr.open("GET",requestURL,true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); 
xhr.responseType = 'json';
xhr.send(); 

// onload 是等資料拿到的時候才執行
xhr.onload = function(){
    // maskdata 設為拿到的回傳 JSON 
    let maskdata = xhr.response.features; // 這邊要觀察一下回傳的資料結構長什麼樣子
    // *****等拿到資料再初始化頁面
    // *****如果沒有這樣寫,則會跟前面同時執行,就會因為沒資料而出錯
    console.log(maskdata);
    console.log(`有 ${maskdata.length} 筆資料`);
}

https://ithelp.ithome.com.tw/upload/images/20200923/20129873obSwKyerKU.png
不過因為語法還沒有很熟,我還是習慣不要一步到位,
一步一步慢慢來。
這邊確定拿得到資料再繼續往下:

// onload 是等資料拿到的時候才執行
xhr.onload = function(){
    // maskdata 設為拿到的回傳 JSON 
    let maskdata = xhr.response.features;
    // *****等拿到資料再初始化頁面
    // *****如果沒有這樣寫,則會跟前面同時執行,就會因為沒資料而出錯
    console.log(maskdata);
    console.log(`有 ${maskdata.length} 筆資料`);
    initialData(maskdata);
}

function initialData(maskdata){
    sendBtnElement.addEventListener("click", getInputValue);
    function getInputValue(){
        console.log("-點到送出按鈕了");
        console.log(`輸入的值為: ${inputNumElement.value}`);
        console.log(`輸入的值 type 為: ${typeof(inputNumElement.value)}`);
    
        let inputNum = parseInt(inputNumElement.value);
        let resultHTMLStr = "";
        for ( let i=0; i<maskdata.length; i++ ){
            if ( maskdata[i].properties.mask_adult > inputNum ){
                resultHTMLStr += `<li>位於 ${maskdata[i].properties.county} ${maskdata[i].properties.town} 的 ${maskdata[i].properties.name} 還有 ${maskdata[i].properties.mask_adult} 個成人口罩</li>`;
            }
        }
        filterResultElement.innerHTML = resultHTMLStr;
    }
}

https://ithelp.ithome.com.tw/upload/images/20200923/20129873apadIJbajZ.png
本日打完收工!

這邊整理一下比較重要的概念:

  1. 拿到回傳資料存成 maskdata 後,再將 maskdata 當成函數帶入值傳入,
    之後要執行的邏輯寫在函數裡,而非 xhr.onload 裡。
    原因程式的註解有寫到,乍看之下程式寫的先後順序好像有先後順序,
    但程式執行速度太快,
    雖然你寫在前後行,但其實是同時執行的,
    為了讓程式有執行先後順序,要用函數去帶。
  2. 所以連帶 getInputValue 的函數也要包在 initialData(maskdata) 裡,
    不然 getInputValue 會不認得 maskdata 是誰。
    ( maskdata是存在 xhr.onload 中的變數 )
    ↑其實我剛自己又踩了這個洞一遍orz

[後記]

有關更多可以玩的 Open Data 可以到 政府資料開放平臺 搜尋,
但裡面很多資料集可能不是那麼方便使用.....orz

因為動態問資料回來做應用是屬於比較進階的寫法了,
我想明天再複習個會比較好~
那就明日再見啦:D

[本日程式碼]

Codepen


上一篇
Day16 - 雖然很久沒有國際賽事,還是要用 JavaScript 關心一下小戴的 World Rankings!─ addEventListener 篇
下一篇
Day18 - 正規表示式學好就可以知道如何正確的表達心意是否搞錯了什麼
系列文
30天找回寫程式手感計劃!!!36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言